package com.github.zyflzz.mycalculator.core;

import android.os.Build;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

import com.github.zyflzz.mycalculator.recyclerview.Record;

import org.jetbrains.annotations.Contract;

import java.util.Stack;

/**
 * @author created by zyf on 2023/3/21
 **/
public class Calculator {

    /**
     * 结果字符串
     */
    private String resultString;

    /**
     * 运算结果数值
     */
    private Double result;

    /**
     * 输入字符串
     */
    private String inputString = "";

    /**
     * 历史记录
     */
    private final Stack<Record> historyStack = new Stack<>();

    /**
     * 是否计算完成
     */
    private boolean isCalcOK = false;

    /**
     * 数栈
     */
    private final Stack<Double> numberStack = new Stack<>();

    /**
     * 操作符栈
     */
    private final Stack<String> operationStack = new Stack<>();

    // 两个辅助变量，卡塔兰常数和欧拉常数

    private static final Double catalan = 0.915965594177219015054603;
    private static final Double eul_mas = 0.577215664901532860606512;

    /**
     * Calculator 唯一实例
     */
    private volatile static Calculator calculator;

    private Calculator() {
    }

    /**
     * 双检锁实现，保证多线程的安全性
     *
     * @return {@link #calculator}
     */
    public static Calculator getCalculator() {
        if (calculator == null) {
            synchronized (Calculator.class) {
                if (calculator == null) {
                    calculator = new Calculator();
                }
            }
        }
        return calculator;
    }

    public Stack<Record> getHistoryStack() {
        return historyStack;
    }

    /**
     * <p>计算</p>
     * <p>该方法是一个上层封装，实际调用该了 {@link #scan(String)} 方法来扫描字符串并进行计算</p>
     *
     * @param input 输入的字符串
     * @return 结果字符串
     */
    @RequiresApi(api = Build.VERSION_CODES.O)
    public String calc(String input) throws Exception {
        inputString = input;

        // 扫描字符串，进行计算
        scan(input);
        result = numberStack.peek();
        if (result < 1E-10) {
            result = 0.0;
        }
        resultString = String.valueOf(result);

        // 将结果写入历史记录
        Record record = new Record(inputString.replaceAll(" ", ""), resultString);
        historyStack.push(record);

        // 计算完成后，栈中的数据就没有用了，为了不影响以后的使用，需要清空栈
        numberStack.clear();
        operationStack.clear();
        this.isCalcOK = true;

        return resultString;
    }

    public void clean() {
        numberStack.clear();
        operationStack.clear();
        this.isCalcOK = false;
        result = 0.0;
        resultString = "";
        inputString = "";
    }

    public Double getResult() {
        if (isCalcOK) {
            return result;
        } else return 0.0;
    }

    public String getResultString() {
        if (isCalcOK) {
            return resultString;
        } else return "";
    }

    /**
     * 扫描字符串
     *
     * @param input 输入的字符串
     */
    private void scan(@NonNull String input) throws Exception {
        this.isCalcOK = false;
        String[] inputList = input.split(" ");
        for (String s : inputList) {
            if (s.equals(Operation.LB.getSymbol())) operationStack.push(s);
            else if (s.equals(Operation.ADD.getSymbol()) || s.equals(Operation.MINUS.getSymbol())) {
                if (!operationStack.empty()) hlopIntegration();
                operationStack.push(s);
            } else if (isHighLevelOp(s)) {
                operationStack.push(s);
            } else if (!operationStack.empty() && s.equals(Operation.RB.getSymbol())) {
                while (!operationStack.peek().equals(Operation.LB.getSymbol())) opIntegration();
                if (!operationStack.empty() && operationStack.peek().equals(Operation.LB.getSymbol()))
                    operationStack.pop();
                if (!operationStack.empty()) hlopIntegration();
            } else if (s.equals(Operation.DEG.getSymbol())) {
                numberStack.push(numberStack.pop() * Math.PI / 180);
            } else if (s.equals(Operation.PERC.getSymbol())) {
                numberStack.push(numberStack.pop() / 100);
            } else if (s.equals(Operation.FRAC.getSymbol())) {
                numberStack.push(frac(numberStack.pop()));
            } else if (s.equals(Operation.PI.getSymbol())) {
                numberStack.push(Math.PI);
            } else if (s.equals(Operation.E.getSymbol())) {
                numberStack.push(Math.E);
            } else if (s.equals(Operation.CATALAN.getSymbol())) {
                numberStack.push(catalan);
            } else if (s.equals(Operation.EM.getSymbol())) {
                numberStack.push(eul_mas);
            } else {
                numberStack.push(Double.valueOf(s));
            }
        }
        while (!operationStack.empty()) {
            opIntegration();
        }
    }

    // ************************** //
    // 注意在堆栈中的运算顺序是逆序的！

    private void add() {
        operationStack.pop();
        Double n2 = numberStack.pop();
        Double n1 = numberStack.pop();
        numberStack.push(n1 + n2);
    }

    private void minus() {
        operationStack.pop();
        Double n2 = numberStack.pop();
        Double n1 = numberStack.pop();
        numberStack.push(n1 - n2);
    }

    private void times() {
        operationStack.pop();
        Double n2 = numberStack.pop();
        Double n1 = numberStack.pop();
        numberStack.push(n1 * n2);
    }

    private void div() {
        operationStack.pop();
        Double n2 = numberStack.pop();
        Double n1 = numberStack.pop();
        numberStack.push(n1 / n2);
    }

    private void pow() {
        operationStack.pop();
        Double n2 = numberStack.pop();
        Double n1 = numberStack.pop();
        numberStack.push(Math.pow(n1, n2));
    }

    private void sin() {
        operationStack.pop();
        numberStack.push(Math.sin(numberStack.pop()));
    }

    private void cos() {
        operationStack.pop();
        numberStack.push(Math.cos(numberStack.pop()));
    }

    private void tan() {
        operationStack.pop();
        numberStack.push(Math.tan(numberStack.pop()));
    }

    private void ln() {
        operationStack.pop();
        numberStack.push(Math.log(numberStack.pop()));
    }

    private void lg() {
        operationStack.pop();
        numberStack.push(Math.log10(numberStack.pop()));
    }

    private void sqrt() {
        operationStack.pop();
        numberStack.push(Math.sqrt(numberStack.pop()));
    }

    // 2nd

    private void asin() {
        operationStack.pop();
        numberStack.push(Math.asin(numberStack.pop()));
    }

    private void acos() {
        operationStack.pop();
        numberStack.push(Math.acos(numberStack.pop()));
    }

    private void atan() {
        operationStack.pop();
        numberStack.push(Math.atan(numberStack.pop()));
    }

    private void sinh() {
        operationStack.pop();
        numberStack.push(Math.sinh(numberStack.pop()));
    }

    private void cosh() {
        operationStack.pop();
        numberStack.push(Math.cosh(numberStack.pop()));
    }

    private void tanh() {
        operationStack.pop();
        numberStack.push(Math.tanh(numberStack.pop()));
    }

    private void square() {
        operationStack.pop();
        Double n = numberStack.pop();
        numberStack.push(n * n);
    }

    private void gamma() {
        operationStack.pop();
        Double n = numberStack.pop();
        n = gamma(n, 0.00001);
        numberStack.push(n);
    }

    private void exp() {
        operationStack.pop();
        numberStack.push(Math.exp(numberStack.pop()));
    }


    // ************************** //

    private boolean isHighLevelOp(@NonNull String s) {
        return s.equals(Operation.TIMES.getSymbol())
                || s.equals(Operation.DIV.getSymbol())
                || s.equals(Operation.POW.getSymbol())
                || s.equals(Operation.SIN.getSymbol())
                || s.equals(Operation.COS.getSymbol())
                || s.equals(Operation.TAN.getSymbol())
                || s.equals(Operation.LG.getSymbol())
                || s.equals(Operation.LN.getSymbol())
                || s.equals(Operation.SQRT.getSymbol())
                || s.equals(Operation.ASIN.getSymbol())
                || s.equals(Operation.ACOS.getSymbol())
                || s.equals(Operation.ATAN.getSymbol())
                || s.equals(Operation.SINH.getSymbol())
                || s.equals(Operation.COSH.getSymbol())
                || s.equals(Operation.TANH.getSymbol())
                || s.equals(Operation.EXP.getSymbol())
                || s.equals(Operation.SQUARE.getSymbol())
                || s.equals(Operation.GAMMA.getSymbol())
                ;
    }

    private void opIntegration() throws Exception {
        String latestOp = operationStack.peek();
        if (latestOp.equals(Operation.ADD.getSymbol())) add();
        else if (latestOp.equals(Operation.MINUS.getSymbol())) minus();
        else if (latestOp.equals(Operation.TIMES.getSymbol())) times();
        else if (latestOp.equals(Operation.DIV.getSymbol())) div();
        else if (latestOp.equals(Operation.POW.getSymbol())) pow();
        else if (latestOp.equals(Operation.SIN.getSymbol())) sin();
        else if (latestOp.equals(Operation.COS.getSymbol())) cos();
        else if (latestOp.equals(Operation.TAN.getSymbol())) tan();
        else if (latestOp.equals(Operation.LN.getSymbol())) ln();
        else if (latestOp.equals(Operation.LG.getSymbol())) lg();
        else if (latestOp.equals(Operation.SQRT.getSymbol())) sqrt();
        else if (latestOp.equals(Operation.SQUARE.getSymbol())) square();
        else if (latestOp.equals(Operation.ASIN.getSymbol())) asin();
        else if (latestOp.equals(Operation.ACOS.getSymbol())) acos();
        else if (latestOp.equals(Operation.ATAN.getSymbol())) atan();
        else if (latestOp.equals(Operation.SINH.getSymbol())) sinh();
        else if (latestOp.equals(Operation.COSH.getSymbol())) cosh();
        else if (latestOp.equals(Operation.TANH.getSymbol())) tanh();
        else if (latestOp.equals(Operation.EXP.getSymbol())) exp();
        else if (latestOp.equals(Operation.GAMMA.getSymbol())) gamma();
        else throw new Exception();
    }

    private void llopIntegration() {
        String latestOp = operationStack.peek();
        if (latestOp.equals(Operation.ADD.getSymbol())) add();
        else if (latestOp.equals(Operation.MINUS.getSymbol())) minus();
    }

    private void hlopIntegration() {
        String latestOp = operationStack.peek();
        if (latestOp.equals(Operation.TIMES.getSymbol())) times();
        else if (latestOp.equals(Operation.DIV.getSymbol())) div();
        else if (latestOp.equals(Operation.POW.getSymbol())) pow();
        else if (latestOp.equals(Operation.SIN.getSymbol())) sin();
        else if (latestOp.equals(Operation.COS.getSymbol())) cos();
        else if (latestOp.equals(Operation.TAN.getSymbol())) tan();
        else if (latestOp.equals(Operation.LN.getSymbol())) ln();
        else if (latestOp.equals(Operation.LG.getSymbol())) lg();
        else if (latestOp.equals(Operation.SQRT.getSymbol())) sqrt();
        else if (latestOp.equals(Operation.SQUARE.getSymbol())) square();
        else if (latestOp.equals(Operation.ASIN.getSymbol())) asin();
        else if (latestOp.equals(Operation.ACOS.getSymbol())) acos();
        else if (latestOp.equals(Operation.ATAN.getSymbol())) atan();
        else if (latestOp.equals(Operation.SINH.getSymbol())) sinh();
        else if (latestOp.equals(Operation.COSH.getSymbol())) cosh();
        else if (latestOp.equals(Operation.TANH.getSymbol())) tanh();
        else if (latestOp.equals(Operation.EXP.getSymbol())) exp();
        else if (latestOp.equals(Operation.GAMMA.getSymbol())) gamma();
    }

    @NonNull
    @Contract(pure = true)
    private Double frac(Double value) {
        return Double.valueOf(frac(Math.toIntExact(Math.round(value))));
    }

    @NonNull
    @Contract(pure = true)
    private Integer frac(Integer integer) {
        if (integer > 1) {
            return integer * frac(integer - 1);
        } else {
            return 1;
        }
    }

    public double gamma(double x, double setAbsRelaErr) {
        //setAbsRelaErr 相对误差绝对值
        //递归结束条件
        if (x < 0) {
            return gamma(x + 1, setAbsRelaErr) / x;
        }
        if (Math.abs(1.0 - x) < 0.00001) {
            return 1;
        }
        if (Math.abs(0.5 - x) < 0.00001) {
            return Math.sqrt(3.1415926);
        }

        if (x > 1.0) {
            return (x - 1) * gamma(x - 1, setAbsRelaErr);
        }
        double res = 0.0;
        double temp = 1.0;
        double check = 0.0;
        int i = 1;
        while (Math.abs((check - temp) / temp) > setAbsRelaErr) {
            check = temp;
            temp *= i / (x - 1 + i);
            i++;
        }
        res = temp * Math.pow(i, x - 1);
        return res;
    }


    public String getInputString() {
        return inputString;
    }
}
